/*
 * Copyright (C) 2014 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { Headers } from "./Headers"
import { MediaType } from "./MediaType"
import { Sink } from "./okio/Sink"
import { RequestBody } from "./RequestBody"
import { kotlinCheck, kotlinRequire } from "./utils/Utils"
import { util } from "@kit.ArkTS"
import { StringToUint8Array } from "./utils/StringUtil"

/**
 * An [RFC 2387][rfc_2387]-compliant request body.
 *
 * [rfc_2387]: http://www.ietf.org/rfc/rfc2387.txt
 */
export class MultipartBody extends RequestBody {

  static readonly MIXED = MediaType.toMediaType("multipart/mixed")
  static readonly ALTERNATIVE = MediaType.toMediaType("multipart/alternative")
  static readonly DIGEST = MediaType.toMediaType("multipart/digest")
  static readonly PARALLEL = MediaType.toMediaType("multipart/parallel")
  static readonly FORM = MediaType.toMediaType("multipart/form-data")

  static readonly DASHDASH = new Uint8Array([0x2d, 0x2d])  // '--'
  static readonly CRLF = new Uint8Array([0x0d, 0x0a])  // '\r\n'
  static readonly COLONSPACE = new Uint8Array([0x3a, 0x20])  // ': '

  private readonly boundaryByteString: string
  private readonly _type: MediaType
  get type(): MediaType {
    return this._type
  }
  private readonly _parts: Array<Part>
  get parts(): Array<Part> {
    return this._parts
  }
  private readonly _contentType: MediaType
  private _contentLength = -1

  private constructor(boundaryString: string,type: MediaType, parts: Array<Part>) {
    super()
    this.boundaryByteString = boundaryString
    this._type = type
    this._parts = parts
    this._contentType = MediaType.toMediaType(`${this._type}; boundary=${boundaryString}`)
  }



  get boundary() {
    return this.boundaryByteString
  }

  /** The number of parts in this multipart body. */
  get size() {
    return this.parts.length
  }

  part(index: number): Part {
    return this.parts[index]
  }

  /** A combination of [type] and [boundaryByteString]. */
  override contentType(): MediaType | null {
    return this._contentType
  }


  // @Throws(IOException::class)
  override async contentLength() {
    let result = this._contentLength
    if (result === -1) {
      result = await this.writeOrCountBytes(null, true)
      this._contentLength = result
    }
    return result
  }

  // @Throws(IOException::class)
  override async writeTo(sink: Sink) {
    await this.writeOrCountBytes(sink, false)
  }

  /**
   * Either writes this request to [sink] or measures its content length. We have one method do
   * double-duty to make sure the counting and content are consistent, particularly when it comes
   * to awkward operations like measuring the encoded length of header strings, or the
   * length-in-digits of an encoded integer.
   */
  // @Throws(IOException::class)
  private async writeOrCountBytes(sink: Sink, countBytes: boolean): Promise<number> {
    let byteCount = 0
    let buffers: Uint8Array[] | null = []
    let boundaryBytes = StringToUint8Array(this.boundaryByteString);
    // 统一写入函数：根据 mode 决定是收集 buffer 还是写入 sink
    const write = async (data: Uint8Array | string) => {
      if (typeof data === 'string') {
        data = StringToUint8Array(data);
      }
      if (countBytes) {
        buffers?.push(data);
      } else {
        await sink.write(data, data.byteLength); // 假设 sink 有 write(Uint8Array)
      }
    };

    for (const part of this.parts) {
      const headers = part.headers
      const body = part.body
      await write(MultipartBody.DASHDASH)
      await write(boundaryBytes)
      await write(MultipartBody.CRLF)
      if (headers !== null && headers !== undefined) {
        for (let h = 0; h < headers.size; h++) {
          await write(StringToUint8Array(headers.name(h)))
          await write(MultipartBody.COLONSPACE)
          await write(StringToUint8Array(headers.value(h)))
          await write(MultipartBody.CRLF)
        }
      }
      let contentType = body.contentType()
      if (contentType !== null && contentType !== undefined) {
        await write(StringToUint8Array("Content-Type: "))
        await write(StringToUint8Array(contentType.toString()))
        await write(MultipartBody.CRLF)
      }

      let contentLength = await body.contentLength()
      if (contentLength !== -1) {
        await write(StringToUint8Array("Content-Length: "))
        await write(StringToUint8Array(contentLength.toString()))
        await write(MultipartBody.CRLF)
      }else if (countBytes) {
        // We can't measure the body's size without the sizes of its components.
        buffers = null
        return -1
      }
      await write(MultipartBody.CRLF)

      if (countBytes) {
        byteCount += contentLength
      } else {
        await body.writeTo(sink)
      }

      await write(MultipartBody.CRLF)
    }
    await write(MultipartBody.DASHDASH)
    await write(boundaryBytes)
    await write(MultipartBody.DASHDASH)
    await write(MultipartBody.CRLF)

    if (countBytes) {
      byteCount += buffers.reduce((total, arr) => total + arr.length, 0)
    }
    return byteCount
  }


  static Builder = class{
    private boundary: string
    private type = MultipartBody.MIXED
    private parts: Array<Part> = []

    constructor(boundary: string = util.generateRandomUUID(true)){
      this.boundary = boundary
    }

    /**
     * Set the MIME type. Expected values for `type` are [MIXED] (the default), [ALTERNATIVE],
     * [DIGEST], [PARALLEL] and [FORM].
     */
    setType(type: MediaType): this {
      kotlinRequire(type.type === "multipart", `multipart != ${type}`)
      this.type = type
      return this
    }
    addPart(body: RequestBody): this
    addPart(headers: Headers, body: RequestBody): this
    addPart(part: Part): this
    /** Add a part to the body. */
    addPart(var1: RequestBody | Part | Headers, body?: RequestBody): this {
      if (var1 instanceof RequestBody) {
        this.parts.push(Part.create(var1))
      } else if (var1 instanceof Part) {
        this.parts.push(var1)
      } else if (var1 instanceof Headers) {
        this.parts.push(Part.create(var1, body!))
      }
      return this
    }
    addFormDataPart(name: string, value: string): this
    addFormDataPart(name: string, filename: string, body: RequestBody): this
    /** Add a form data part to the body. */
    addFormDataPart(name: string, value: string, body?: RequestBody): this {
      if (body) {
        this.addPart(Part.createFormData(name, value, body))
      } else {
        this.addPart(Part.createFormData(name, value))
      }
      return this
    }

    /** Assemble the specified parts into a request body. */
    build(): MultipartBody {
      kotlinCheck(this.parts.length !== 0, "Multipart body must have at least one part.")
      return new MultipartBody(this.boundary, this.type, this.parts)
    }
  }
}


export class Part {
  private readonly _headers?: Headers
  get headers(): Headers | undefined {
    return this._headers
  }
  private readonly _body: RequestBody
  get body(): RequestBody {
    return this._body
  }
  constructor(headers: Headers | undefined, body: RequestBody) {
    this._headers = headers;
    this._body = body;
  }

  static create(body: RequestBody): Part
  static create(headers: Headers, body: RequestBody): Part
  static create(var1: RequestBody | Headers, var2?: RequestBody): Part {
    let headers: Headers | undefined
    let body: RequestBody

    if (var1 instanceof RequestBody) {
      body = var1
      headers = undefined
    } else {
      headers = var1
      body = var2!
      kotlinRequire(headers.get("Content-Type") === null, "Unexpected header: Content-Type")
      kotlinRequire(headers.get("Content-Length") === null, "Unexpected header: Content-Length")
    }

    return new Part(headers, body)
  }
  static createFormData(name: string, value: string): Part
  static createFormData(name: string, filename: string, body: RequestBody): Part
  static createFormData(var1: string, var2?: string, var3?: RequestBody): Part {
    const name: string = var1
    const filename: string | null = var3 === undefined ? null : var2!
    const body: RequestBody = var3 === undefined ? RequestBody.StringToRequestBody(var2!) : var3
    
    const dispositionParts: string[] = ["form-data; name="]
    dispositionParts.push(appendQuotedString("", name))
    if (filename !== null) {
      dispositionParts.push("; filename=")
      dispositionParts.push(appendQuotedString("", filename))
    }
    const disposition = dispositionParts.join("")
    
    const headers = new Headers.Builder()
      .addUnsafeNonAscii("Content-Disposition", disposition)
      .build()

    return this.create(headers, body)
  }
}

function appendQuotedString(disposition: string, key: string): string {
  const parts: string[] = [disposition, '"']
  for (const ch of key) {
    if (ch === '\n') {
      parts.push("%0A")
    } else if (ch === '\r') {
      parts.push("%0D")
    } else if (ch === '"') {
      parts.push("%22")
    } else {
      parts.push(ch)
    }
  }
  parts.push('"')
  return parts.join("")
}